package com.iakuil.app.common.restful;

import com.iakuil.app.common.core.constant.SysConstant;
import com.iakuil.app.common.core.domain.BaseCmd;
import com.iakuil.app.common.core.domain.BaseQuery;
import com.iakuil.app.common.core.enums.Results;
import com.iakuil.app.common.core.exception.BusinessException;
import com.iakuil.app.common.core.util.*;
import io.swagger.v3.oas.annotations.Hidden;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;

import java.io.IOException;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Date;
import java.util.Map;

/**
 * 全局JSON参数处理
 *
 * @author Kai
 */
@ControllerAdvice
@Hidden
public class GlobalRequestBodyAdvice implements RequestBodyAdvice {

    private static final String[] AUDITING_FIELDS = {"createTime", "createBy", "updateTime", "updateBy"};

    private static final Class[] ANNOS = {RequestMapping.class, PostMapping.class, DeleteMapping.class, PutMapping.class};

    @Override
    public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        AnnotatedElement element = methodParameter.getAnnotatedElement();
        return Arrays.stream(ANNOS).anyMatch(anno -> anno.isAnnotation() && element.isAnnotationPresent(anno));
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) throws IOException {
        return httpInputMessage;
    }

    @Override
    public Object afterBodyRead(Object o, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        handleJsonBody(o);
        return o;
    }

    @Override
    public Object handleEmptyBody(Object o, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return o;
    }

    private void handleJsonBody(Object obj) {
        if (obj == null) {
            return;
        }

        Class<?> clazz = obj.getClass();
        if (clazz.isAssignableFrom(Map.class)) {
            // 给开发看的，如果确实有需要，请使用DynamicField
            throw new BusinessException(Results.BAD_REQUEST.getCode(), "不允许使用Map作为接口参数");
        }

        // 处理属性过滤参数，注意：URL参数优先级比JSON中的高
        // e.g. http://localhost:10240?fields=id,createTime
        String fields = ServletUtils.getParameter(SysConstant.PROP_FILTER);
        if (StringUtils.isNotEmpty(fields) && obj instanceof BaseQuery query) {
            query.setFields(StringUtils.split(fields, StringUtils.COMMA));
        }

        // 如果入参是BaseCmd的子类，将会进行严格校验
        if (obj instanceof BaseCmd cmd) {
            // 时间戳校验，默认相差不能超过1分钟
            if (cmd.getTs() == null || DateUtils.MILLIS_PER_MINUTE < DateUtils.between(new Date(), new Date(cmd.getTs() * 1000))) {
                throw new BusinessException(Results.BAD_REQUEST.getCode(), "无效的时间戳");
            }

            // 还是给开发看的，审计字段神圣不可侵犯！
            if (ArrayUtils.containsAny(ReflectUtils.getFields(obj.getClass()), AUDITING_FIELDS)) {
                throw new BusinessException(Results.FORBIDDEN.getCode(), "不允许操作审计字段");
            }
        }
    }
}
